%%
%% Copyright (c) 2017, 2018 Dmitry Poroh
%% All rights reserved.
%% Distributed under the terms of the MIT License. See the LICENSE file.
%%
%% Generating unique transaction identifier.
%%

-module(ersip_trans_id).

-export([make_server/1,
         make_server_cancel/1,
         make_server_cancel/2,
         make_client/2
        ]).
-export_type([transaction_id/0]).

%%%===================================================================
%%% Types
%%%===================================================================

-record(tid_rfc3261, {branch_id :: ersip_branch:branch_key(),
                      sent_by   :: ersip_hdr_via:sent_by(),
                      method    :: ersip_method:method()
                     }).
-record(tid_rfc2543,
        {callid      :: ersip_hdr_callid:callid(),
         ruri        :: ersip_uri:uri(),
         from_tag    :: ersip_hdr_fromto:tag_key() | undefined,
         to_tag      :: ersip_hdr_fromto:tag_key() | undefined,
         cseq        :: ersip_hdr_cseq:cseq(),
         topmost_via_key :: ersip_hdr_via:via_key()
        }).
-type tid_rfc3261() :: #tid_rfc3261{}.
-type tid_rfc2543() :: #tid_rfc2543{}.
-type tid_client()  :: {ersip_branch:branch_key(), ersip_method:method()}.
-type transaction_id() :: tid_rfc3261()
                        | tid_rfc2543()
                        | tid_client().

%%%===================================================================
%%% API
%%%===================================================================

-spec make_server(ersip_sipmsg:sipmsg()) -> transaction_id().
make_server(SipMsg) ->
    %% The branch parameter in the topmost Via header field of the
    %% request is examined.  If it is present and begins with the magic
    %% cookie "z9hG4bK", the request was generated by a client
    %% transaction compliant to this specification.
    TopmostVia = ersip_sipmsg:get(topmost_via, SipMsg),
    case ersip_hdr_via:branch(TopmostVia) of
        {ok, Branch} ->
            case is_rfc3261(Branch) of
                true ->
                    make_rfc3261_tid(TopmostVia, SipMsg);
                false ->
                    make_rfc2543_tid(TopmostVia, SipMsg)
            end;
        undefined ->
            make_rfc2543_tid(TopmostVia, SipMsg)
    end.

%% @doc Create transaction id of cancelled INVITE request by CANCEL SIP
%% message.
-spec make_server_cancel(ersip_sipmsg:sipmsg()) -> transaction_id().
make_server_cancel(CancelSipMsg) ->
    make_server_cancel(CancelSipMsg, ersip_method:invite()).

%% @doc Create transaction id of cancelled request by CANCEL SIP
%% message.  Method is method of request to be cancelled. Most common
%% is ersip_method:invite() here.
-spec make_server_cancel(ersip_sipmsg:sipmsg(), ersip_method:method()) -> transaction_id().
make_server_cancel(CancelSipMsg, Method) ->
    %% The CANCEL method requests that the TU at the server side
    %% cancel a pending transaction.  The TU determines the
    %% transaction to be cancelled by taking the CANCEL request, and
    %% then assuming that the request method is anything but CANCEL or
    %% ACK and applying the transaction matching procedures of Section
    %% 17.2.3.  The matching transaction is the one to be cancelled.
    TID = make_server(CancelSipMsg),
    replace_method(Method, TID).

-spec make_client(ersip_branch:branch(), ersip_method:method()) -> transaction_id().
make_client(Branch, Method) ->
    {ersip_branch:make_key(Branch), Method}.

%%%===================================================================
%%% Internal implementation
%%%===================================================================

-spec make_rfc3261_tid(ersip_hdr_via:via(), ersip_sipmsg:sipmsg()) -> tid_rfc3261().
make_rfc3261_tid(TopmostVia, Message) ->
    %% The request matches a transaction if:
    %%
    %%    1. the branch parameter in the request is equal to the one in the
    %%       top Via header field of the request that created the
    %%       transaction, and
    %%
    %%    2. the sent-by value in the top Via of the request is equal to the
    %%       one in the request that created the transaction, and
    %%
    %%    3. the method of the request matches the one that created the
    %%       transaction, except for ACK, where the method of the request
    %%       that created the transaction is INVITE.
    {ok, Branch} = ersip_hdr_via:branch(TopmostVia),
    SentByKey    = ersip_hdr_via:sent_by_key(TopmostVia),
    Method       = ersip_sipmsg:method(Message),
    ACK          = ersip_method:ack(),
    EffMethod =
        case Method of
            ACK -> ersip_method:invite();
            M -> M
        end,
    #tid_rfc3261{method  = EffMethod,
                 sent_by = SentByKey,
                 branch_id = ersip_branch:make_key(Branch)}.

-spec make_rfc2543_tid(ersip_hdr_via:via(), ersip_sipmsg:sipmsg()) -> tid_rfc2543().
make_rfc2543_tid(TopmostVia, Message) ->
    Method = ersip_sipmsg:method(Message),
    RURI   = ersip_sipmsg:ruri(Message),
    CallId = ersip_sipmsg:get(callid, Message),
    From   = ersip_sipmsg:get(from, Message),
    To     = ersip_sipmsg:get(to,   Message),
    CSeq   = ersip_sipmsg:get(cseq, Message),
    INVITE = ersip_method:invite(),
    ACK    = ersip_method:ack(),
    case Method of
        INVITE ->
            %% The INVITE request matches a transaction if the
            %% Request-URI, To tag, From tag, Call-ID, CSeq, and top
            %% Via header field match those of the INVITE request
            %% which created the transaction.  In this case, the
            %% INVITE is a retransmission of the original one that
            %% created the transaction.
            #tid_rfc2543{callid          = ersip_hdr_callid:make_key(CallId),
                         ruri            = ersip_uri:make_key(RURI),
                         from_tag        = ersip_hdr_fromto:tag_key(From),
                         to_tag          = ersip_hdr_fromto:tag_key(To),
                         cseq            = ersip_hdr_cseq:make_key(CSeq),
                         topmost_via_key = ersip_hdr_via:make_key(TopmostVia)
                        };
        ACK ->
            %% The ACK request matches a transaction if the Request-
            %% URI, From tag, Call-ID, CSeq number (not the method), and top Via
            %% header field match those of the INVITE request which created the
            %% transaction, and the To tag of the ACK matches the To tag of the
            %% response sent by the server transaction.
            INVITE = ersip_method:invite(),
            ACKCSeq = ersip_hdr_cseq:make(INVITE, ersip_hdr_cseq:number(CSeq)),
            #tid_rfc2543{callid          = ersip_hdr_callid:make_key(CallId),
                         ruri            = ersip_uri:make_key(RURI),
                         from_tag        = ersip_hdr_fromto:tag_key(From),
                         to_tag          = undefined,
                         cseq            = ACKCSeq,
                         topmost_via_key = ersip_hdr_via:make_key(TopmostVia)
                        };
        _ ->
            %% For all other request methods, a request is matched to
            %% a transaction if the Request-URI, To tag, From tag,
            %% Call-ID, CSeq (including the method), and top Via
            %% header field match those of the request that created
            %% the transaction.  Matching is done based on the
            %% matching rules defined for each of those header fields.
            #tid_rfc2543{callid          = ersip_hdr_callid:make_key(CallId),
                         ruri            = ersip_uri:make_key(RURI),
                         from_tag        = ersip_hdr_fromto:tag_key(From),
                         to_tag          = ersip_hdr_fromto:tag_key(To),
                         cseq            = ersip_hdr_cseq:make_key(CSeq),
                         topmost_via_key = ersip_hdr_via:make_key(TopmostVia)
                        }
    end.

%% If the branch parameter in the top Via header field is
%% not present, or does not contain the magic cookie, the
%% following procedures are used.  These exist to handle
%% backwards compatibility with RFC 2543 compliant
%% implementations.
-spec is_rfc3261(ersip_branch:branch()) -> boolean().
is_rfc3261(Branch) ->
    ersip_branch:is_rfc3261(Branch).

-spec replace_method(ersip_method:method(), transaction_id()) -> transaction_id().
replace_method(Method, #tid_rfc3261{} = TID) ->
    TID#tid_rfc3261{method = Method};
replace_method(Method, #tid_rfc2543{cseq = CSeq} = TID) ->
    %% RFC 2543:
    %% The Call-ID, To, the numeric part of CSeq and From headers in
    %% the CANCEL request are identical to those in the original
    %% request. This allows a CANCEL request to be matched with the
    %% request it cancels.  However, to allow the client to
    %% distinguish responses to the CANCEL from those to the original
    %% request, the CSeq Method component is set to CANCEL. The Via
    %% header field is initialized to the proxy issuing the CANCEL
    %% request. (Thus, responses to this CANCEL request only reach the
    %% issuing proxy.)
    NewCSeq = ersip_hdr_cseq:make(Method, ersip_hdr_cseq:number(CSeq)),
    TID#tid_rfc2543{cseq = NewCSeq}.

